package com.dd.cloud.gateway.filter;

import com.dd.cloud.common.config.common.ProfilesProperties;
import com.dd.cloud.common.config.cookie.JwtProperties;
import com.dd.cloud.common.constants.HealthConstants;
import com.dd.cloud.common.constants.RedisKeyConstants;
import com.dd.cloud.common.enums.ZkExceptionEnum;
import com.dd.cloud.common.exception.ZekeException;
import com.dd.cloud.common.utils.JsonUtils;
import com.dd.cloud.common.vo.LoginInfo;
import com.dd.cloud.common.vo.LoginInfoTransfer;
import com.dd.cloud.gateway.config.AesencryptProperties;
import com.dd.cloud.gateway.config.ParamsHandle;
import com.dd.cloud.gateway.feign.AuthFeignService;
import com.dd.cloud.gateway.properties.FilterProperties;
import com.dd.cloud.gateway.util.CookieUtils;
import com.dd.cloud.gateway.util.JwtUtils;
import com.dd.cloud.gateway.util.PathUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @Description: springcloud gateway全局过滤器
 * @Author: czg
 * @Date: 2023/5/13 14:14
 * @Version: 1.0.0
 */
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    @Autowired
    private FilterProperties filterProperties;
    @Autowired
    private JwtProperties prop;
    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Value("${server.servlet.context-path}")
    private String applicationName;
    @Autowired
    private AuthFeignService authFeignService;

    public static final ThreadLocal<String> loginInfoTransferThreadLocal = new ThreadLocal<>();
    @Autowired
    private ParamsHandle paramsHandle;
    @Autowired
    private AesencryptProperties aesencryptProperties;
    @Autowired
    private ProfilesProperties profilesProperties;

    /**
     * @param exchange:
     * @param chain:
     * @return Mono<Void>
     * @description gateway过滤器, 用于身份验证,信息传递, 参数加密
     * @author czg
     * @date 2023/5/16 11:52
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String path = request.getPath().toString();
        String userAgent = jwtUtils.getUserAgent(request);
        if (isAllowPath(path)) {
            /**
             * 1, 运行逻辑,网关过滤每条请求, 如果是不拦截登录则放行,跳到4
             * 2, 登录拦截->获取对应的cookie值,并解析成登录对象
             * 3, 判断cookie, 是否过期,是否要刷新,是否要下线, 都通过则有请求权限,
             * 4, 将登录信息封装,转成json,并转码(有中文),写入到request的heaer里面,并转发改请求(服务间通信一样是http)
             * 5, 业务service接收到请求,从header中获取登录信息,并转码,并判断登录信息是否合法
             */
            LoginInfoTransfer loginInfoTransfer = new LoginInfoTransfer();
            loginInfoTransfer.setLoginInfo(null);
            loginInfoTransfer.setAllowPath(false);
            String loginUser = JsonUtils.objectToStr(loginInfoTransfer);
            loginInfoTransferThreadLocal.set(loginUser);


            Mono<Void> voidMono = decryptParams(exchange, chain);
            return voidMono;
        }

        //内部服务接口，不允许外部访问
//        if(antPathMatcher.match("/**/inner/**", path)) {
//            throw new ZekeException(ZkExceptionEnum.NO_ERMSSION);
//        }
        HttpHeaders headers = request.getHeaders();

        String client = headers.getFirst("client");
        String cookieName = jwtUtils.getCookieName(client);
        HttpCookie cookie = CookieUtils.getCookieValue(request, cookieName);
        if (cookie == null) {
            throw new ZekeException(ZkExceptionEnum.LOGIN_AGAIN);
        }
        //获取携带的cookie, 值是token
        String token = cookie.getValue();
        LoginInfo loginInfo = null;
        //是否刷新cookie
        boolean renovate = true;
        try {
            loginInfo = jwtUtils.getInfoFromToken(token);
            String useToken = redisTemplate.opsForValue().get(client + loginInfo.getType() + "-" + loginInfo.getId() + "-" + loginInfo.getType());
            //loginOut配合登出和退出,删除使用
            if ("loginOut".equals(useToken)) {
                CookieUtils.set(response, jwtUtils.getCookieName(request), null, 0);
            }
            if (StringUtils.isBlank(useToken)) {
                throw new ZekeException(ZkExceptionEnum.LOGIN_AGAIN);
            } else if (!useToken.equals(token)) {
                //redis key的old 刷新cookie有效期时可能会有其他的请求进来,带来的还是旧的cookie, 会造成redis的值不一致,暂时旧的加个缓存时间
                useToken = redisTemplate.opsForValue().get(client + loginInfo.getType() + "-" + loginInfo.getId() + "-" + loginInfo.getType() + "-old");
                if (!token.equals(useToken)) {
                    CookieUtils.set(response, jwtUtils.getCookieName(request), null, 0);
                    log.info("账号其他地方登陆,下线\n存在的token=" + useToken + "\n携带的token=" + token);
                    if (loginInfo.getId() != 4 || loginInfo.getType() != LoginInfo.UserType.ADMIN) {
                        throw new ZekeException(ZkExceptionEnum.LOGIN_ELSEWHERE);
                    }
                }
                renovate = false;
            }

            /**
             * 客户端标识不一致, 要重新登录
             */
            if (userAgent != null && !userAgent.equals(loginInfo.getUserAgent())) {
                log.debug("当前客户端标识:{}", userAgent);
                log.debug("登录的客户端标识:{}", loginInfo.getUserAgent());
                if (profilesProperties.getActive().equals(HealthConstants.ProfilesType.PROD)) {
                    //throw new ZekeException(ZkExceptionEnum.LOGIN_PARSING_ERROR);
                }
            }

            // 时间，当还有一半时间过期时，刷新token
            long dx = jwtUtils.calcTokenTimeLeft(token);
            log.info("token还剩{}分钟过期：", dx);
            if (dx * 2 < prop.getExpire() && renovate) {
                // 解析token
                LoginInfo infoFromToken = jwtUtils.getInfoFromToken(token);
                // 刷新token并重新写入
                String newToken = jwtUtils.generateToken(infoFromToken);
                if (!StringUtils.isBlank(newToken)) {
                    //设置旧cookie 缓存一分钟
                    redisTemplate.opsForValue().set(client + loginInfo.getType() + "-" + loginInfo.getId() + "-" + loginInfo.getType() + "-old", token, 1, TimeUnit.MINUTES);
                    String sameSite = headers.getFirst("sameSite");
                    ;
                    CookieUtils.newBuilder(response).httpOnly().request(request).build(jwtUtils.getCookieName(request), newToken, sameSite);
                    //重新写入redis 并设置有效期
                    redisTemplate.opsForValue().set(client + loginInfo.getType() + "-" + loginInfo.getId() + "-" + loginInfo.getType(), newToken);
                    redisTemplate.expire(client + loginInfo.getType() + "-" + loginInfo.getId() + "-" + loginInfo.getType(), prop.getExpire(), TimeUnit.MINUTES);
                    if (loginInfo.getType() == LoginInfo.UserType.ADMIN || loginInfo.getType() == LoginInfo.UserType.PHAR) {
                        //管理员, 对权限菜单设置一样的有效期
                        redisTemplate.expire(RedisKeyConstants.CacheKey.AUTHROLEMENU + loginInfo.getRoleId(), prop.getExpire(), TimeUnit.MINUTES);
                    }

                }
            }
            LoginInfoTransfer loginInfoTransfer = new LoginInfoTransfer();
            loginInfoTransfer.setLoginInfo(loginInfo);
            String loginUser = JsonUtils.objectToStr(loginInfoTransfer);
            loginInfoTransferThreadLocal.set(loginUser);
            /**
             * 如果是运营管理员和药房管理员, 要验证是否用户对当前uri是否有权限
             * 分两个逻辑,1,用户调用的其他服务,需要先经过auth服务验证是否有权限,此时要验证的uri单独参数就存放于req中
             * 2,用户调用的就是auth服务,则不用验证,到auth服务会自动验证,此时要验证的uri就是req的uri
             */
            if (loginInfo.getType() == LoginInfo.UserType.ADMIN
                    || loginInfo.getType() == LoginInfo.UserType.PHAR
                    || loginInfo.getType() == LoginInfo.UserType.MECHAN) {
                path = path.substring(path.indexOf(applicationName) + applicationName.length());
                if (!path.startsWith("/auth")) {
                    ResponseEntity<Map<String, String>> booleanResponseEntity = authFeignService.checkRoleToUrl(URLEncoder.encode(loginUser, "utf-8"), path);
                    if (!StringUtils.isBlank(booleanResponseEntity.getBody().get("status"))) {
                        throw new ZekeException(booleanResponseEntity.getBody().get("message"));
                    }
                }
            }

        } catch (ZekeException e) {
            e.printStackTrace();
            log.info("异常");
            throw e;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析失败 未登录");
            throw new ZekeException(ZkExceptionEnum.LOGIN_PARSING_ERROR);
        }
        Mono<Void> voidMono = decryptParams(exchange, chain);
        return voidMono;
    }

    /**
     * 权重，值越小优先级越高
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 判断是否是过滤的请求
     * 匹配适配* ? 如/user/doctor/login/* 适配/user/doctor/login/pwd 为true
     * 如/user/?/login/pwd 适配/user/doctor/login/pwd 为true
     */
    private boolean isAllowPath(String path) {
        path = path.substring(path.indexOf(applicationName) + applicationName.length());
        List<String> allowPaths = filterProperties.getAllowPaths();
        for (String allowPath : allowPaths) {
            boolean isMatched = PathUtil.match(allowPath, path);
            if (isMatched) {
                return true;
            }
        }
        return false;
    }


    /**
     * @param exchange:
     * @param chain:
     * @return Mono
     * @description 对不同的content-type请求参数作不同加密解析, 并把解析后的参数串设置到请求体里
     * @author czg
     * @date 2023/12/11 17:31
     */
    public Mono decryptParams(ServerWebExchange exchange, GatewayFilterChain chain) {
        //客户端唯一标识, 并不完全唯一
        String userAgent = jwtUtils.getUserAgent(exchange.getRequest());
        ServerHttpRequest authorization = exchange.getRequest().mutate().headers(httpHeaders ->
                {
                    try {
                        //消息头添加用户信息了客户端唯一信息, 传递给其他服务
                        httpHeaders.set("User-Agent", userAgent);
                        //添加登录信息
                        httpHeaders.add(HealthConstants.CommonConstants.LOGIN_INFO, URLEncoder.encode(AuthGlobalFilter.loginInfoTransferThreadLocal.get(), "utf-8"));
                    } catch (UnsupportedEncodingException e) {
                        log.error("设置登录信息失败,{}", AuthGlobalFilter.loginInfoTransferThreadLocal.get());
                    }
                }
        ).build();
        exchange.mutate().request(authorization);
        ServerHttpResponse response = exchange.getResponse();
        response.beforeCommit(() -> Mono.fromRunnable(() -> {
            //添加响应头, AesParam为true 通知前端参数要加解密  false不加解密
            HttpHeaders headers = exchange.getResponse().getHeaders();
            if (headers.containsKey(PathUtil.ENCRYPT_HANDER)) {
                headers.set(PathUtil.ENCRYPT_HANDER, aesencryptProperties.getParamEncryptAble().toString());
            } else {
                headers.add(PathUtil.ENCRYPT_HANDER, aesencryptProperties.getParamEncryptAble().toString());
            }
        }));
        Mono<Void> mono = chain.filter(exchange.mutate().response(response).build());
        ServerHttpRequest request = exchange.getRequest();
        MediaType contentType = request.getHeaders().getContentType();
        if (contentType != null) {
            log.debug("content-type:{},{},{}", contentType.getType(), contentType.getSubtype(), contentType.getCharset());
        } else {
            log.debug("content-type:为空");
        }
        String url = request.getURI().toString();
        log.info("url:{},{}", request.getMethod().toString(), url);
        // 加密请求头, 这个请求头的值为设定的值代表加解密, 其他值都不加解密
        String product = request.getHeaders().getFirst(PathUtil.ENCRYPT_HANDER);
        log.debug("参数是否要解密:{}", product);
        if (!"true".equals(product)) {
            return mono;
        }

        //不加解密的请求
        if (PathUtil.handleExcludeUrl(request, aesencryptProperties.getRepeatSubLet())) {
            return mono;
        }

        /**
         * post请求 只对json和x-www-form-urlencoded格式请求加解密,其他请求暂不做处理
         */
        if (Objects.nonNull(contentType) && Objects.nonNull(exchange.getRequest().getMethod())
                && exchange.getRequest().getMethod().equals(HttpMethod.POST)) {
            if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
                log.debug("json参数解密");
                // json 请求体处理
                mono = paramsHandle.jsonDecrypt(exchange, chain);
            } else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
                log.debug("xw参数解密");
                // x-www-form-urlencoded 格式处理
                mono = paramsHandle.xwFromBody(exchange, chain);
            }
        } else {
            //get请求 保持原样
            if (exchange.getRequest().getMethod().equals(HttpMethod.GET)) {
                Map<String, String> queryParams = exchange.getRequest().getQueryParams().toSingleValueMap();
                log.info("url地址参数:{}", queryParams);
            }
        }
        return mono;

    }
}
